#!/usr/bin/env python
#
#
#  Change	Date		Author			Description
# --------------------------------------------------------------------------------------------------------
#   001	        07-13-2013      Adam Dosch              Initial release
#   002         07-14-2013      Adam Dosch              Creating buildQueueQuery() function to build queue
#                                                       search pattern
#                                                       Updated temporary path to find configuration file
#                                                       Added buildQueryQueue() to generate custom mongoDB
#                                                       query based on ostypes and archtypes combinations
#   003         07-15-2013      Adam Dosch              Working on queue logic to put incoming queue items
#                                                       into the build collection and update their status
#                                                       accordingly
#                                                       Removed trial logic for getting records off of mongoDB
#                                                       capped queue --- exists in really early revisions
#   004         07-16-2013      Adam Dosch              Adding roughed-in logic to move a matching queue object
#                                                       over to build collection and flag it.
#                                                       Adjusting buildQueryQueue() to inlcude 'processed':'N'
#                                                       JSON flag so it doesn't grab records already processed
#                                                       in the queue if the daemon is restarted and new tail
#                                                       cursor is established.
#   005         07-21-2013      Adam Dosch              Adding loging to work on building RPMs after they are
#                                                       fetched from the queue and moved to build collection
#   006         08-04-2013      Adam Dosch              Changed 'processed' build/queue JSON document option
#                                                       (which signified if item was processed or picked up by
#                                                       a build daemon) to 'pickedUp' instead
#                                                       Updating queue pickup time 'pickedUpTime' document option
#                                                       queue update statement with current date/time
#   007         10-08-2013      Adam Dosch              Adding datetime import for updating 'pickedUpTime' option
#                                                       Setting 'pickedUp' state from N/Y to False/True to be more
#                                                       consistent with the overall document design
#                                                       Importing 'uuid' module to append unique randomly generated
#                                                       ID onto the build document to keep track of when the build
#                                                       dameon can pick up builds in cetain build states (created,
#                                                       in-progress, etc.) if the uuid has changed --- weak way to
#                                                       address a build-daemon being stopped or restarted at the moment.
#                                                       Roughing in logic for 'heartbeat' collection
#                                                       
#                                                       
#

import sys
import os
#import multiprocessing

from multiprocessing import Process, Queue, Pool

import time

import ConfigParser

import datetime

import uuid

import random

# PyMongo
try:
    from pymongo import errors as DatabaseErrors
    import pymongo
except ImportError:
    print "Install pymongo, please."

# RBS
try:
    from RBS import Config
    from RBS import BuildObject
    from system import Database
except ImportError:
    print "Update sys.path to load RBS modules, please."

#configuration = None
#dbconn = None

def daemonize():
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError:
        print "fork() failed"
        sys.exit(1)

    os.chdir("/")
    os.setsid()
    # Make sure I can read my own files and shut out others
    #prev= os.umask(0)
    #os.umask(prev and int('077',8))
    os.umask(0)
    
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError:
        print "fork() failed"
        sys.exit(1)

    dev_null = file('/dev/null', 'r')
    os.dup2(dev_null.fileno(), sys.stdin.fileno())

def write2log(handle, msg):
    handle.write(msg + "\n")
    handle.flush()

def buildQueueQuery(ostypes, archtypes):
    """
    Build custom queue search criteria --- defines what to search for in queue
    to build.
    
    The designed queue search criteria for a build daemon service will be
    derived with 'or' logic based on what is defined in 'build' section for
    both 'ostypes' 'archtypes'.
   
    Example: If 'ostypes' contains 'el5,el6' and 'archtypes' contains 'x86_64'
             then the search criteria will be:
   
                 el5/x86_64 'or' el6/x86_64
    """

    if isinstance(ostypes, str):
        print "converting ostypes"
        ostypes = [ostypes]
    
    if isinstance(archtypes, str):
        print "converting archtypes"
        archtypes = [archtypes]

    or_list = []

    for ostype in ostypes:
        and_option = {'$and': []}
        
        for arch in archtypes:
            and_option = {'$and': [ {'ostype': ostype, 'arch': arch} ] }
            
            or_list.append(and_option)
    
    return {'$or': or_list, 'pickedUp': False}

def performBuild(buildItem):
    time.sleep(6)
    print "  --> Performed build for %s (%s)" % (buildItem.package, buildItem.id)
    
    #try:
    #    # Lets set up our database connection
    #    global dbconn
    #    dbconn = Database.Connection("builder", server=configuration.getConfiguration('rbs')['dbserver'])
    #    write2log(f, "Successfully connected to DB")
    #except DatabaseErrors.ConnectionFailure, e:
    #    write2log(f, "Problem connecting to database, dude: %s" % e)
        
    builds = dbconn.db['builds']
    buildItem.build['starttime'] = datetime.datetime.now()
    buildItem.build['state'] = "building"
    
    print buildItem.getCurrentBuildStatus()
    
    builds.save(buildItem.getCurrentBuildStatus(), w=1, j=True)
    
def main():
    #print type(configuration.getConfiguration('rbs')['logfile'])
    global f
    f = open(configuration.getConfiguration('rbs')['logfile'], "w")
    
    write2log(f, "Starting main()")

    daemon_uuid = uuid.uuid4()

    write2log(f, "Generated UUID of %s" % daemon_uuid)

    try:
        # Lets set up our database connection
        global dbconn
        dbconn = Database.Connection("builder", server=configuration.getConfiguration('rbs')['dbserver'])
        write2log(f, "Successfully connected to DB")
    except DatabaseErrors.ConnectionFailure, e:
        write2log(f, "Problem connecting to database, dude: %s" % e)
    
    # Establish connections to build and queue collections
    queue = dbconn.db['queue']
    builds = dbconn.db['builds']
    
    # If heartbeat is enabled, lets connect to that collection
    # --- probably should write a web service that authenticates via UUID from RBS service
    #     but this will work for now since we need to connect to mongo to tail capped collection
    if bool(configuration.getConfiguration('rbs')['heartbeat']) == True:
        heartbeat = dbconn.db['heartbeat']
        write2log(f, "Configured to transmit heartbeat, do it to it! %s" % configuration.getConfiguration('rbs')['heartbeat'])
    
    # list for build processes - not needed?
    ###buildProcs = []
    
    write2log(f, "Entering DB connection loop...")
    
    while dbconn.conn.alive:
        write2log(f, " --- DB conn is alive, lets establish tail cursor on queue...")
        
        # build off defined configuration for 'ostype' and 'arch'
        q = buildQueueQuery(configuration.getConfiguration('build')['ostypes'], configuration.getConfiguration('build')['archtypes'])
        
        #tcursor = queue.find({'ostype': 'el6'}, await_data=True, tailable=True, timeout=False)
        tcursor = queue.find(q, await_data=True, tailable=True, timeout=False)
        
        # Lets setup our Worker Pool to build incoming request(s)
        buildPoolWorkers = Pool(processes=int(configuration.getConfiguration('build')['numbuilds']))
        
        # Lets establish a multiprocessing queue to add our in-coming build objects to
        # - Note: this queue will always have a zero balance and will be added to worker Pool()
        #         It's just an easy way to gather all the buildObjects and assign them to a
        #         defined working pool.  I guess any datatype would work ...
        buildQueue = Queue()
        
        # Do we have anything in a non-completed state (e.g. 'created', 'building', 'distributing', etc.)
        # that we should attempt to rebuild
        # - TODO
        
        # Infinite loop on tailing cursor to find us something to build
        while tcursor.alive:
            write2log(f, "  |__ tail cursor alive, lets fetch what's on the queue")
            
            try:
                # Get next build request(s) from queue
                buildrequest = tcursor.next()
                
                # Create the build object for processing
                b = BuildObject(buildrequest)
                
                # Move the build request over to the build collection immediately                
                # Create build object for processing (insert if it doesnt exist, update if it had the same _id)
                builds.save(b.getCurrentBuildStatus(), w=1, j=True)
                
                # Add build object to local multiprocessing-queue
                buildQueue.put(b)
                
                # Get last error to make sure save worked
                #last_error = dbconn.conn.
                #write2log(f, "Last error: %s" % last_error)
                
                # Update queue record to 'pickedUp: True' --- so we are sure to never grab it again
                queue.update({'_id': b.id}, { '$set': {'pickedUp': True, 'pickedUpTime': datetime.datetime.now() } })
                
                write2log(f, "Successfully processed and moved to build collection: %s" % b)
                
                # Printout to see how many we have in our local queue to process
                try:
                    write2log(f, "We have %s in our local multiprocessing queue" % buildQueue.qsize())
                except NotImplementedError:
                    pass
                
            except StopIteration:
                write2log(f, "    |__  No data left to fetch, lets sleep a bit and block")
                time.sleep(1)
            
            finally:
                # Printout to see how many we have in our local queue to process
                write2log(f, "    |__  Our queue size must be greater than zero: %s" % buildQueue.qsize())
                
                if buildQueue.qsize() > 0:
                    write2log(f, "       |__  It is, lets kick off our workers to build these packages")
                    results = buildPoolWorkers.map_async(performBuild, [ buildQueue.get_nowait() for item in range(1, buildQueue.qsize() + 1, 1) ])
                
        write2log(f, "DB connection totally died.  Exiting...")

if __name__ == '__main__':
    # Look for config in these locations
    for f in ['/home/adosch/code/python/remote-build-service','/tmp', '/etc/rbs', '/etc']:
        f += "/rbs-build.cfg"
        
        if os.path.isfile(f):
            if os.access(f, os.R_OK):
                
                global configuration
                configuration = Config(f)
                
                try:
                    configuration.loadConfiguration()
                    print "loading config - %s" % f
                except:
                    raise "Error reading configuration file, dude!"
                
                break

    daemonize()
    main()